/**
 * Copyright (c) 2011 Nokia Corporation.
 */

// Own header
#include "backupengine.h"

// Qt includes
#include <QDir>
#include <QTimer>

// QtMobility includes
#include <QSystemDeviceInfo>

// Backup2Go includes
#include "drivemodel.h"
#include "filecopyoperation.h"
#include "filemanager.h"
#include "filemodel.h"
#include "trace.h"

// Constants
const QString BTGDefaultTargetRootFolderName("backuptogo");

QTM_USE_NAMESPACE;


/*!
  \class BackupEngine
  \brief Implements the application engine visible to the UI.
*/


/*!
  Constructor.
*/
BackupEngine::BackupEngine(QObject *parent /* = 0 */)
    : m_fileManager(0),
      m_sourceFileModel(0),
      m_driveModel(0),
      m_currentSourcePath(QString()),
      m_targetDrive(QString()),
      m_backupProgressState(Idle),
      m_cachedTotalSize(0)
{
    m_driveModel = new DriveModel(this);
    m_fileManager = new FileManager(m_driveModel, this);
    m_sourceFileModel = new FileModel(this);

    connect(this, SIGNAL(targetDriveChanged(QString)),
            this, SLOT(refreshBackedUpFiles(QString)));
    connect(this, SIGNAL(sourcePathChanged(QString)),
            this, SLOT(refreshSourceFileModelStatuses()));

    // Populate the source file model.
    setSourceDrive(0);
}


/*!
  Destructor.
*/
BackupEngine::~BackupEngine()
{
}


/*!
  Returns the source file model.
*/
QAbstractListModel *BackupEngine::sourceFileModel() const
{
    return m_sourceFileModel;
}


/*!
  Returns the drive model.
*/
QAbstractListModel *BackupEngine::driveModel() const
{
    return m_driveModel;
}


/*!
  Returns the current source path in a string.
*/
QString BackupEngine::sourcePath() const
{
    return m_currentSourcePath;
}


/*!
  Returns the current target drive in a string.
*/
QString BackupEngine::targetDrive() const
{
    return m_targetDrive;
}


/*!
  Returns the number of files marked to be backed up.
*/
int BackupEngine::numberOfMarkedFiles() const
{
    return m_markedFiles.count();
}


/*!
  Calculates and returns the total size of the files marked to be backed up.
*/
qlonglong BackupEngine::totalSizeOfMarkedFiles()
{
    const int count(m_markedFiles.count());
    qlonglong size(0);
    QFileInfo info;

    for (int i = 0; i < count; ++i) {
        info = m_markedFiles[i];
        size += info.size();
    }

    m_cachedTotalSize = size;
    return size;
}


/*!
  Returns the current backup progress state as integer.
*/
int BackupEngine::backupProgressState() const
{
    return m_backupProgressState;
}


/*!
  Refreshes the drive model.
*/
void BackupEngine::refreshDrives()
{
    DEBUG_POINT;
    QMetaObject::invokeMethod(m_fileManager, "updateDrives");
}


/*!
  Sets the source drive to the one in the drive model with \a index.
  In addition, the method populates the source file model with the root content
  of the new drive.
*/
void BackupEngine::setSourceDrive(int index)
{
    DEBUG_INFO(index);

    QString driveVolume(m_driveModel->driveByIndex(index));
    DEBUG_INFO("Trying to set" << driveVolume << "as the new source drive.");

    if (!driveVolume.isEmpty()) {
        QDir rootDir(driveVolume);
        m_fileManager->populateModel(*m_sourceFileModel, rootDir);
        m_currentSourcePath = rootDir.absolutePath();
        emit sourcePathChanged(m_currentSourcePath);
    }
}


/*!
  Sets the target drive to the one in the drive model with \a index.
*/
void BackupEngine::setTargetDrive(int index)
{
    DEBUG_INFO(index);
    m_targetDrive = m_driveModel->driveByIndex(index);
    emit targetDriveChanged(m_targetDrive);
}


/*!
  Marks the item with \a index in the source file model to be backed up.
*/
void BackupEngine::selectSourceFileModelIndex(int index)
{
    DEBUG_INFO(index);

    if (m_backupProgressState == InProgress) {
        DEBUG_INFO("Backup in progress, selecting files not allowed!");
        return;
    }

    QFileInfo fileInfo;
    bool success = m_sourceFileModel->getFileInfo(index, fileInfo);

    if (success && fileInfo.isDir()) {
        // Get the status of the file/folder.
        FileModel::Status status = m_sourceFileModel->fileStatus(index);
        DEBUG_INFO("The status of the item is" << (int)status);

        if (status == FileModel::None) {
            // Mark the file/folder to be backed up.
            DEBUG_INFO("Marking the items to be backed up!");

            // First, append the selected file/folder into the list of marked files.
            m_markedFiles.append(fileInfo);
            m_sourceFileModel->setFileStatus(index,
                                             FileModel::MarkedToBeBackedUp);

            // Then append the folder content recursively.
            m_fileManager->getFolderContent(QDir(fileInfo.absoluteFilePath()),
                                            m_markedFiles, true);

            // Remove possible duplicates.
            int count(m_markedFiles.count());

            for (int i = 0; i < count; ++i) {
                for (int j = i + 1; j < count; ++j) {
                    if (m_markedFiles[i] == m_markedFiles[j]) {
                        m_markedFiles.removeAt(j);
                        count = m_markedFiles.count();
                        DEBUG_INFO("Removed duplicate" <<
                                   m_markedFiles[i].absoluteFilePath() <<
                                   "at [" << j << "]");
                    }
                }
            }
        }
        else if (status == FileModel::MarkedToBeBackedUp) {
            // Unmark the file/folder.
            DEBUG_INFO("Unmarking the items from being backed up!");

            m_markedFiles.removeOne(fileInfo);
            m_sourceFileModel->setFileStatus(index, FileModel::None);

            QFileInfoList filesToUnmark;
            m_fileManager->getFolderContent(QDir(fileInfo.absoluteFilePath()),
                                            filesToUnmark, true);

            const int count(filesToUnmark.count());

            for (int i = 0; i < count; ++i) {
                if (m_markedFiles.contains(filesToUnmark[i])) {
                    m_markedFiles.removeOne(filesToUnmark[i]);
                }
            }
        }
        else if (status == FileModel::MarkedToBeRemoved) {
            // Unmark the file/folder from being removed.

            DEBUG_INFO("Unmarking items from being removed not implemented!");
        }
        else if (status == FileModel::BackedUp) {
            // Mark the file/folder to be removed.

            DEBUG_INFO("Marking items to be removed not implemented!");
        }

        // Emit the signals.
        emit numberOfMarkedFilesChanged(m_markedFiles.count());
        emit totalSizeOfMarkedFilesChanged(totalSizeOfMarkedFiles());
    }
#ifdef DEBUG
    else {
        DEBUG_INFO("Will not try to mark/unmark the file; not a folder!");
    }
#endif
}


/*!
  If item in the source file model with \a index is a directory, the source
  file model is re-populated with the contents of that directory.
*/
void BackupEngine::navigateToSourceFolderByIndex(int index)
{
    DEBUG_INFO(index);

    QFileInfo fileInfo;
    bool success = m_sourceFileModel->getFileInfo(index, fileInfo);

    if (success && fileInfo.isDir()) {
        QString newPath(fileInfo.absoluteFilePath());
        m_fileManager->populateModel(*m_sourceFileModel, QDir(newPath));
        m_currentSourcePath = newPath;
        emit sourcePathChanged(m_currentSourcePath);
    }
#ifdef DEBUG
    else {
        DEBUG_INFO("Failed: succss ==" << (int)success <<
                   "; fileInfo.isDir() ==" << (int)fileInfo.isDir());
    }
#endif
}


/*!
  Jumps to the parent folder in the source model. Returns true if success,
  false otherwise which usually indicates that the current path is the root
  and no parent folder exists.
*/
bool BackupEngine::navigateToSourceParentFolder()
{
    DEBUG_POINT;

    QFileInfo fileInfo(m_currentSourcePath);
    QDir parentDirectory(fileInfo.dir());
    QString parentDirectoryPath(parentDirectory.absolutePath());

    if (parentDirectoryPath.compare(m_currentSourcePath)) {
        // The parent directory path is different compared to the current path.
        m_fileManager->populateModel(*m_sourceFileModel, parentDirectory);
        m_currentSourcePath = parentDirectoryPath;
        emit sourcePathChanged(m_currentSourcePath);
        return true;
    }

    return false;
}


/*!
  Clears the list of files marked to be backed up.
*/
void BackupEngine::clearMarkedFiles()
{
    m_markedFiles.clear();
    m_sourceFileModel->clearFileStatuses();
    emit numberOfMarkedFilesChanged(0);
    emit totalSizeOfMarkedFilesChanged(0);
}


/*!
  Starts the backup operation.
*/
bool BackupEngine::startBackup()
{
    if (m_backupProgressState == InProgress ||
        !m_markedFiles.count() || m_targetDrive.isEmpty()) {
        DEBUG_INFO("The backup is already in progress, no files marked to " <<
                   "be backed up or no target drive selected!");
        return false;
    }

    // Construct the copy operation, connect the signals and run.
    FileCopyOperation *copyOp =
        new FileCopyOperation(m_markedFiles, targetRootPath(), this);

    connect(this, SIGNAL(cancelBackup()), copyOp, SLOT(cancel()));
    connect(copyOp, SIGNAL(status(int, qlonglong, QString, QString)),
            this, SLOT(handleCopyStatus(int, qlonglong, QString, QString)));
    connect(copyOp, SIGNAL(failed(QFile::FileError, QString)),
            this, SLOT(handleCopyFailed(QFile::FileError, QString)));
    connect(copyOp, SIGNAL(cancelled()), this, SLOT(handleBackupCancelled()));
    connect(copyOp, SIGNAL(finished()), this, SLOT(handleBackupFinished()));

    // Note that the instance will destroy itself once finished.
    copyOp->start();

    // Set the state.
    setBackupProgressState(InProgress);
    emit progress(0);

    return true;
}


/*!
  Refreshes the list of backed up files. If \a driveVolume is not empty, the
  backed up files will be searched for from the given drive.
*/
void BackupEngine::refreshBackedUpFiles(
    const QString &driveVolume /* = QString() */)
{
    DEBUG_INFO(driveVolume);

    if (m_backedUpFiles.count()) {
        // Clear the old content.
        m_backedUpFiles.clear();
    }

    if (QFile::exists(targetRootPath())) {
        // Traverse the target root path.
        m_fileManager->getFolderContent(QDir(targetRootPath(driveVolume)),
                                        m_backedUpFiles, true);
    }
#ifdef DEBUG
    else {
        DEBUG_INFO("No target root path found from drive.");
    }
#endif

    refreshSourceFileModelStatuses();
}


/*!
  Refreshes the file statuses in the source file model.
*/
void BackupEngine::refreshSourceFileModelStatuses()
{
    DEBUG_POINT;

    QFileInfoList files(m_sourceFileModel->files());

    const int count(files.count());
    QMap<int, FileModel::Status> statuses;

    for (int i = 0; i < count; ++i) {
        // Check for marked files.
        if (m_markedFiles.contains(files[i])) {
            statuses.insert(i, FileModel::MarkedToBeBackedUp);
        }
        else if (files[i].isDir()) {
            // Check if any of the marked files exist in the current folder.
            QString path(files[i].absoluteFilePath());
            const int markedFilesCount(m_markedFiles.count());
            QString markedFilePath;

            for (int j = 0; j < markedFilesCount; ++j) {
                markedFilePath = m_markedFiles[j].absoluteFilePath();

                if (markedFilePath.startsWith(path)) {
                    statuses.insert(i, FileModel::ContainsMarkedToBeBackedUp);
                    break;
                }
            }
        }

        // Check for backed up files.
        const int backedUpFilesCount(m_backedUpFiles.count());
        const int rootLength(targetRootPath().length() + 1); // + 1 for '/'
        QString backedUpFilePath;
        QString path(files[i].absoluteFilePath());

        for (int j = 0; j < backedUpFilesCount; ++j) {
            backedUpFilePath = m_backedUpFiles[j].absoluteFilePath();
            backedUpFilePath.remove(0, rootLength);

            if (backedUpFilePath[1] == '/') {
                // The path contains the drive volume letter. Add the missing
                // ':' for comparison.
                backedUpFilePath.insert(1, ':');
#ifdef DEBUG
                if (j == 0) {
                    // Show the path.
                    DEBUG_INFO("The paths take form like" << backedUpFilePath);
                }
#endif
            }

            if (!path.compare(backedUpFilePath)) {
                statuses.insert(i, FileModel::BackedUp);
                break;
            }
        }
    }

    m_sourceFileModel->setFileStatuses(statuses);
}


/*!
  Sets the backup progress state to \a state.
*/
void BackupEngine::setBackupProgressState(
    BackupEngine::BackupProgressState state /* = Idle */)
{
    DEBUG_INFO((int)state);
    m_backupProgressState = state;
    emit backupProgressStateChanged(m_backupProgressState);
}


/*!
  Resets the backup progress state to Idle.
*/
void BackupEngine::resetBackupProgressState()
{
    DEBUG_POINT;

    if (m_backupProgressState != InProgress) {
        setBackupProgressState(Idle);
    }
}


/*!
  Handles the copy operation status signals.
*/
void BackupEngine::handleCopyStatus(int index,
                                    qlonglong totalSizeCopied,
                                    const QString &fileCopied,
                                    const QString &fileStarting)
{
    Q_UNUSED(index);
    Q_UNUSED(fileCopied);
    Q_UNUSED(fileStarting);

    int percentsComplete((int)qRound(100 * totalSizeCopied / m_cachedTotalSize));

#ifdef DEBUG
    if (index % 10) {
        DEBUG_INFO(percentsComplete << "% complete");
    }
#endif

    emit progress(percentsComplete);
}


/*!
  Handle the file error occured when copying \a filename. \a error contains
  a possible error code.
*/
void BackupEngine::handleCopyFailed(QFile::FileError error,
                                    const QString &filename)
{
    DEBUG_INFO("Failed when copying" << filename << ":" << error);
    Q_UNUSED(error);
    Q_UNUSED(filename);
    setBackupProgressState(Failed);
}


/*!
  This slot gets called when the backing up has been cancelled.
*/
void BackupEngine::handleBackupCancelled()
{
    DEBUG_POINT;
    setBackupProgressState(Cancelled);
}


/*!
  This slot gets called when backing up files is finished.
*/
void BackupEngine::handleBackupFinished()
{
    DEBUG_INFO("Progress state:" << (int)m_backupProgressState);

    switch (m_backupProgressState) {
        case InProgress: {
            setBackupProgressState(Finished);
            emit progress(100);

            // Clear the list of marked files as they should now be backed up.
            clearMarkedFiles();
            break;
        }
        case Cancelled:
        case Failed:
        default: {
            // Do nothing.
            break;
        }
    }

    refreshBackedUpFiles();
    QTimer::singleShot(3000 /* ms */, this, SLOT(resetBackupProgressState()));
}


/*!
  Returns the target root path depending on \a driveVolume. If \a driveVolume
  is an empty string, the current target drive is used.
*/
QString BackupEngine::targetRootPath(const QString &driveVolume /* = QString() */) const
{
    QString driveRoot(driveVolume);

    if (driveRoot.isEmpty()) {
        // Use the current target drive.
        driveRoot = m_targetDrive;
    }

    if (driveRoot.isEmpty() || driveRoot.length() > 3) {
        // No target drive volume specified.
        driveRoot = "/";
    }
    else {
        // Make sure the drive path ends with ":/".
        if (!driveRoot.endsWith(":/")) {
            driveRoot.append(":/");
        }
    }

    // Use IMEI to select a unique folder.
    QSystemDeviceInfo deviceInfo;
    QString deviceUniqueFolderName(deviceInfo.imei());

    // Construct the full target root path.
    QString rootPath(driveRoot + BTGDefaultTargetRootFolderName +
                     "/" + deviceUniqueFolderName);

    return rootPath;
}


// End of file.
